package org.sosie2.service.framework;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.InvalidPropertiesFormatException;
import java.util.Iterator;
import java.util.Locale;
import java.util.Vector;

import javax.xml.soap.SOAPBody;
import javax.xml.soap.SOAPElement;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPHeader;
import javax.xml.soap.SOAPHeaderElement;

import org.apache.axis.Message;
import org.apache.axis.MessageContext;
import org.apache.axis.message.MessageElement;
import org.apache.axis.message.SOAPEnvelope;
import org.apache.ws.security.WSSecurityEngine;
import org.apache.ws.security.WSSecurityEngineResult;
import org.apache.ws.security.message.WSSecHeader;
import org.apache.ws.security.message.WSSecSAMLToken;
import org.opensaml.SAMLAssertion;
import org.opensaml.SAMLAttribute;
import org.opensaml.SAMLAttributeStatement;
import org.sosie2.service.authentication.CallbackHandler;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

/**
 * 
 * This is the abstract class all web services extend.
 * The reason to do this is because web services share the same 
 * function signatures, but with slightly different functionality
 * 
 * @author Gabriela
 */
public abstract class AbstractService {
		
	public String actor = Utils.ACTOR; 
	
	
	/**
	 * The main method, that is to be exposed over the internet.
	 * It holds all public functionality a web service publishes. 
	 * It calls the invoke method but also treats the Exceptions that may occur
	 * from the invoke with the same parameters  
	 * @param req  - the SOAPEnvelope received as a request
	 * @param resp - the SOAPEnvelope sent further as response 
	 */
	public void serve(SOAPEnvelope req, SOAPEnvelope resp) {
		try {
			// Retrieve the message from the message context
			MessageContext mc = MessageContext.getCurrentContext();
			Message mReq = mc.getRequestMessage();
			Message mResp = mc.getResponseMessage();
			
			mResp = invoke(mReq, mResp);
			
			mc.setResponseMessage(mResp);
		} catch (Exception exception) {
			// Generate SOAP Exception, depending on the type of Exception received
			if (exception instanceof SosieException) {
				handleSosieException(resp, (SosieException)exception);
			} else {
				handleOtherException(resp, exception);
			}
		}
	}
	
	
	/**
	 * This is a example of method to handle SosieExceptions.
	 * A new SOAPBody is generated that includes an AxisFault with the required information  
	 * @param resp - the response SOAPEnvelope to be sent
	 * @param exception - the SosieException being handled
	 */
	protected void handleSosieException(SOAPEnvelope resp, SosieException exception ) {
		try {
			resp.getBody().addFault(resp.createName("Client"), 
									exception.getDetailMessage()+"class:"+exception.getExceptionClass(), 
									new Locale("English"));
		} catch(Exception e) {
			e.printStackTrace();
		}
	}
	
	
	protected void handleOtherException(SOAPEnvelope resp, Exception exception) {
		try {
			resp.getBody().addFault(resp.createName("Client"),exception.getMessage());
		} catch(SOAPException e) {
			e.printStackTrace();
		}
	}
	
	
	/**
	 * Implement the functionallity of the service in this method.
	 * 
	 * @param req 	The request Message 
	 * @param resp 	The response Message 
	 * @return 		The response Message (after it has been processed)
	 * @throws Exception - later to be dealt with by the serve method
	 */
	public abstract Message invoke(Message req, Message resp) throws Exception;
	
// ----------------------------- Get / Set Methods -----------------------------
	
	
	public String getActor() {
		return actor;
	}

	public void setActor(String actor) {
		this.actor = actor;
	}

	
// ----------------------------- Annotation Methods -----------------------------
	
	
	/**
	 * This method generates one annotation with several attribute statements.
	 * @param actor The actor in the annotation (as String)
	 * @param namespace	The namespace in the annotation (as String)
	 * @param attributeNamesToValues A hashmap including pairs of attribute names with vectors of values
	 * @param target A target URI (here as String) associated with every annotation
	 * @return A SAMLAnnotation which is to be added to the response SOAP Envelope
	 * @throws Exception In case anything goes wrong
	 * 
	 * @deprecated
	 */
	protected SAMLAnnotation generateAnnotation(String subjectName, String[][] attributes, String target) throws Exception {
		return new SAMLAnnotation(attributes, subjectName, target);
	}

	
	/**
	 * Getenerateas a SAML Annotation.
	 * 
	 * @param attributes
	 * @param target
	 * @return
	 * @throws Exception
	 * 
	 * @deprecated
	 */
	protected SAMLAnnotation generateAnnotation(String[][] attributes, String target) throws Exception {
		return generateAnnotation(actor, attributes, target);
	}

	
	/**
	 * Appends a new annotation to a SOAP envelope.
	 * 
	 * @param document The SOAP message envelope where the annotation will be appended
	 * @param annotation The annotation to be appended
	 * @param actor The actor of the WS-Security header
	 * @return
	 */
	protected Document addAnnotation(Document document, SAMLAnnotation annotation, String actor) {
		WSSecSAMLToken wsToken = new WSSecSAMLToken();
		WSSecHeader wsHeader = new WSSecHeader(actor, false);
		wsHeader.insertSecurityHeader(document);
		
		//System.out.println("ANNOTATION : " + annotation.getSAMLAssertion().toString());
		document = wsToken.build(document, annotation.getAssertion(), wsHeader);

		return document;
	}

	
	protected Document addAnnotation(Document document, SAMLAnnotation annotation) {
		return addAnnotation(document, annotation, actor);
	}
	
	
	/**
	 * Returns all annotations found in a given SOAP envelope.
	 * 
	 * @param envelope The SOAP message envelope
	 * @return A list of annotations
	 * @throws Exception
	 */
	protected SAMLAnnotation[]  getAllAnnotations(Document envelope) throws Exception
	{		
		ArrayList<SAMLAnnotation> annotations = new ArrayList<SAMLAnnotation>();
		WSSecurityEngine secEngine = WSSecurityEngine.getInstance();
		CallbackHandler callback = null;
		
		Vector results = secEngine.processSecurityHeader(envelope, actor, callback, null);
		
		if (results == null) {
//				System.out.print("No result after processing header !");
			return null;
		}
		
		for (int i = 0; i < results.size(); i++) {
			WSSecurityEngineResult res = (WSSecurityEngineResult)results.get(i);
			SAMLAssertion assertion = res.getAssertion();
			if (assertion != null) {
				SAMLAnnotation newAnn = new SAMLAnnotation(assertion); 
				annotations.add(newAnn);
				
				break;
			}		
		}		
			
		return annotations.toArray(new SAMLAnnotation[2]);
	}
	
	
	/**
	 * Returns all annotations with a specific target.
	 * @param envelope The SOAP message envelope
	 * @param target The target
	 * @return An array list of annotations
	 * @throws Exception
	 */
	protected SAMLAnnotation[] getAnnotations(Document envelope, String target) throws Exception {		
		ArrayList<SAMLAnnotation> annotations = new ArrayList<SAMLAnnotation>();
		WSSecurityEngine secEngine = WSSecurityEngine.getInstance();
		CallbackHandler callback = null;
					
		Vector results = secEngine.processSecurityHeader(envelope, actor, callback, null);
		
		if (results == null) {
			System.out.print("No result after processing header !");
			return null;
		}
		
		for (int i = 0; i < results.size(); i++) {
			WSSecurityEngineResult res = (WSSecurityEngineResult)results.get(i);
			SAMLAssertion assertion = res.getAssertion();
			if (assertion != null) {
				for (Iterator it = assertion.getStatements(); it.hasNext();) {
					Object obj = it.next();
					if (obj instanceof SAMLAttributeStatement) {
						for (Iterator ii = ((SAMLAttributeStatement)obj).getAttributes(); ii.hasNext();) {
							SAMLAttribute at = (SAMLAttribute)ii.next();
							if (at.getName().equals("Target") && at.getValues().next().equals(target)) {
								SAMLAnnotation newAnn = new SAMLAnnotation(assertion); 
								annotations.add(newAnn);
							
								break;
							}
						}
					}
				}
			}		
		}		
		return annotations.toArray(new SAMLAnnotation[2]);
	}
	
	
	/**
	 * Removes an annotation from a given SOAP envelope.
	 * 
	 * @param envelope The SOAP envelope
	 * @param id The annotation ID
	 * @return The SOAP envelope where the annotation was removed
	 * @throws Exception
	 */
	protected SOAPEnvelope deleteAnnotation(SOAPEnvelope envelope, String id) throws Exception {
		org.apache.axis.message.SOAPHeaderElement she = null;
		String token = "Assertion AssertionID=" + "\"" + id + "\"";
		
		boolean stop = false;
		Node toBeRemoved = null;
		
		Vector v = envelope.getHeaders();
		if (v != null)
			for (Iterator it = v.iterator(); it.hasNext() && !stop;) {
				SOAPHeader sh = (SOAPHeader)it.next();
				for (Iterator iit = sh.examineAllHeaderElements();iit.hasNext() && !stop;) {					
					she = (org.apache.axis.message.SOAPHeaderElement)iit.next();
					toBeRemoved = she.getFirstChild();
					//TODO: Implementation is not generic, what if prefix is not wsse!
					if (she.getName().contains("wsse:Security") && toBeRemoved.getNodeName().contains(token)) {
						stop = true;
						break;
					}
				}
			}
		else 
			throw new Exception("SOAP Envelope has no headers!");
		
		if (toBeRemoved != null)
			she.removeChild(toBeRemoved);
		else 		
			throw new Exception("No assertion with the given id!");
		
		return envelope;
	}

	
	/**
	 * Removes an annotation from a given SOAP Envelope
	 * 
	 * @param doc The SOAP Envelope (as Document)
	 * @param id The annotation ID
	 * @return The SOAP Envelope where the annotation has been removed
	 * @throws Exception
	 */
	protected Document deleteAnnotation(Document doc, String id) throws Exception {
		
		if (doc.getChildNodes().getLength() > 0 ) {				
			Element element = (Element)doc.getElementsByTagName("Assertion").item(0);
			// Remove the node
			if (element.getAttribute("AssertionID").contains(id)) {
				element.getParentNode().removeChild(element);
				// Remove all <junk> elements
				Utils.removeAll(doc, Node.ELEMENT_NODE, "Assertion");
				// Remove all comment nodes
				Utils.removeAll(doc, Node.COMMENT_NODE, null);
				// Normalize the DOM tree to combine all adjacent text nodes
				doc.normalize();
			}
		} else {
//			System.out.println("No child nodes for document !");
			throw new Exception("No child nodes to process in the document");
		}
		
		return doc;
	}
	
	
//	 ----------------------------- Convenience Methods -----------------------------
	
	
	/**
	 * Performs deep-copy of a SOAPHeader. 
	 * 
	 * @param from 
	 * @param to 
	 * @throws Exception In case anythig goes wrong
	 */
	protected void copyHeader(SOAPHeader from, SOAPHeader to) throws Exception {
		
		for (Iterator iit = from.examineAllHeaderElements();iit.hasNext();) {
			SOAPHeaderElement she = (SOAPHeaderElement)iit.next();
			if (she != null) {
				SOAPHeaderElement headerElement = to.addHeaderElement(she.getElementName());
				headerElement.setActor(she.getActor());
				for (Iterator i = she.getChildElements(); i.hasNext();) {
					Object obj = i.next();
					if (obj instanceof org.apache.axis.message.Text) {
						String nod = ((org.apache.axis.message.Text)obj).getData();
						headerElement.addTextNode(nod);
					} else {
						MessageElement soapKid = (MessageElement)obj;
						headerElement.addChildElement(soapKid);
					}
				}		
				
			} else 
				throw new Exception("Header Element null in CopyHeader...");
	    }
		
	}
	
	
	/**
	 * Performs deep-copy of a SOAPBody. 
	 * 
	 * @param from 
	 * @param to 
	 * @throws Exception In case anythig goes wrong
	 */
	protected void copyBody(SOAPBody from, SOAPBody to) throws Exception {
		
		 for (Iterator iit = from.getChildElements();iit.hasNext();) {
			SOAPElement she = (SOAPElement)iit.next();
			if (she != null) {
				SOAPElement headerElement = to.addBodyElement(she.getElementName());
				for (Iterator i = she.getChildElements(); i.hasNext();) {
					Object obj = i.next();
					if (obj instanceof org.apache.axis.message.Text) {
						String nod = ((org.apache.axis.message.Text)obj).getData();
						headerElement.addTextNode(nod);
					} else {
						MessageElement soapKid = (MessageElement)obj;
						headerElement.addChildElement(soapKid);
					}
				}						
			} else 
				throw new Exception("Header Element null in CopyHeader...");
	    }		 
	}
	

}
